/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactMount
 */

'use strict'

var DOMLazyTree = require('./DOMLazyTree')
var DOMProperty = require('./DOMProperty')
var ReactBrowserEventEmitter = require('./ReactBrowserEventEmitter')
var ReactCurrentOwner = require('./ReactCurrentOwner')
var ReactDOMComponentTree = require('./ReactDOMComponentTree')
var ReactDOMContainerInfo = require('./ReactDOMContainerInfo')
var ReactDOMFeatureFlags = require('./ReactDOMFeatureFlags')
var ReactElement = require('./ReactElement')
var ReactFeatureFlags = require('./ReactFeatureFlags')
var ReactInstrumentation = require('./ReactInstrumentation')
var ReactMarkupChecksum = require('./ReactMarkupChecksum')
var ReactPerf = require('./ReactPerf')
var ReactReconciler = require('./ReactReconciler')
var ReactUpdateQueue = require('./ReactUpdateQueue')
var ReactUpdates = require('./ReactUpdates')

var emptyObject = require('fbjs/lib/emptyObject')
var instantiateReactComponent = require('./instantiateReactComponent')
var invariant = require('fbjs/lib/invariant')
var setInnerHTML = require('./setInnerHTML')
var shouldUpdateReactComponent = require('./shouldUpdateReactComponent')
var warning = require('fbjs/lib/warning')

var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME
var ROOT_ATTR_NAME = DOMProperty.ROOT_ATTRIBUTE_NAME

var ELEMENT_NODE_TYPE = 1
var DOC_NODE_TYPE = 9
var DOCUMENT_FRAGMENT_NODE_TYPE = 11

var instancesByReactRootID = {}

/**
 * Finds the index of the first character
 * that's not common between the two given strings.
 *
 * @return {number} the index of the character where the strings diverge
 */
function firstDifferenceIndex(string1, string2) {
  var minLen = Math.min(string1.length, string2.length)
  for (var i = 0; i < minLen; i++) {
    if (string1.charAt(i) !== string2.charAt(i)) {
      return i
    }
  }
  return string1.length === string2.length ? -1 : minLen
}

/**
 * @param {DOMElement|DOMDocument} container DOM element that may contain
 * a React component
 * @return {?*} DOM element that may have the reactRoot ID, or null.
 */
function getReactRootElementInContainer(container) {
  if (!container) {
    return null
  }

  if (container.nodeType === DOC_NODE_TYPE) {
    return container.documentElement
  } else {
    return container.firstChild
  }
}

function internalGetID(node) {
  // If node is something like a window, document, or text node, none of
  // which support attributes or a .getAttribute method, gracefully return
  // the empty string, as if the attribute were missing.
  return (node.getAttribute && node.getAttribute(ATTR_NAME)) || ''
}

/**
 * Mounts this component and inserts it into the DOM.
 *
 * @param {ReactComponent} componentInstance The instance to mount.
 * @param {DOMElement} container DOM element to mount into.
 * @param {ReactReconcileTransaction} transaction
 * @param {boolean} shouldReuseMarkup If true, do not insert markup
 */
function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context
) {
  var markerName
  if (ReactFeatureFlags.logTopLevelRenders) {
    var wrappedElement = wrapperInstance._currentElement.props
    var type = wrappedElement.type
    markerName =
      'React mount: ' +
      (typeof type === 'string' ? type : type.displayName || type.name)
    console.time(markerName)
  }

  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context
  )

  if (markerName) {
    console.timeEnd(markerName)
  }

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction
  )
}

/**
 * Batched mount.
 *
 * @param {ReactComponent} componentInstance The instance to mount.
 * @param {DOMElement} container DOM element to mount into.
 * @param {boolean} shouldReuseMarkup If true, do not insert markup
 */
function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context
) {
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    /* useCreateElement */
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement
  )
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context
  )
  ReactUpdates.ReactReconcileTransaction.release(transaction)
}

/**
 * Unmounts a component and removes it from the DOM.
 *
 * @param {ReactComponent} instance React component instance.
 * @param {DOMElement} container DOM element to unmount from.
 * @final
 * @internal
 * @see {ReactMount.unmountComponentAtNode}
 */
function unmountComponentFromNode(instance, container, safely) {
  ReactReconciler.unmountComponent(instance, safely)

  if (container.nodeType === DOC_NODE_TYPE) {
    container = container.documentElement
  }

  // http://jsperf.com/emptying-a-node
  while (container.lastChild) {
    container.removeChild(container.lastChild)
  }
}

/**
 * True if the supplied DOM node has a direct React-rendered child that is
 * not a React root element. Useful for warning in `render`,
 * `unmountComponentAtNode`, etc.
 *
 * @param {?DOMElement} node The candidate DOM node.
 * @return {boolean} True if the DOM element contains a direct child that was
 * rendered by React but is not a root element.
 * @internal
 */
function hasNonRootReactChild(container) {
  var rootEl = getReactRootElementInContainer(container)
  if (rootEl) {
    var inst = ReactDOMComponentTree.getInstanceFromNode(rootEl)
    return !!(inst && inst._nativeParent)
  }
}

function getNativeRootInstanceInContainer(container) {
  var rootEl = getReactRootElementInContainer(container)
  var prevNativeInstance =
    rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl)
  return prevNativeInstance && !prevNativeInstance._nativeParent
    ? prevNativeInstance
    : null
}

function getTopLevelWrapperInContainer(container) {
  var root = getNativeRootInstanceInContainer(container)
  return root ? root._nativeContainerInfo._topLevelWrapper : null
}

/**
 * Temporary (?) hack so that we can store all top-level pending updates on
 * composites instead of having to worry about different types of components
 * here.
 */
var topLevelRootCounter = 1
var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++
}
TopLevelWrapper.prototype.isReactComponent = {}
if (process.env.NODE_ENV !== 'production') {
  TopLevelWrapper.displayName = 'TopLevelWrapper'
}
TopLevelWrapper.prototype.render = function () {
  // this.props is actually a ReactElement
  return this.props
}

/**
 * Mounting is the process of initializing a React component by creating its
 * representative DOM elements and inserting them into a supplied `container`.
 * Any prior content inside `container` is destroyed in the process.
 *
 *   ReactMount.render(
 *     component,
 *     document.getElementById('container')
 *   );
 *
 *   <div id="container">                   <-- Supplied `container`.
 *     <div data-reactid=".3">              <-- Rendered reactRoot of React
 *       // ...                                 component.
 *     </div>
 *   </div>
 *
 * Inside of `container`, the first element rendered is the "reactRoot".
 */
var ReactMount = {
  TopLevelWrapper: TopLevelWrapper,

  /**
   * Used by devtools. The keys are not important.
   */
  _instancesByReactRootID: instancesByReactRootID,

  /**
   * This is a hook provided to support rendering React components while
   * ensuring that the apparent scroll position of its `container` does not
   * change.
   *
   * @param {DOMElement} container The `container` being rendered into.
   * @param {function} renderCallback This must be called once to do the render.
   */
  scrollMonitor: function (container, renderCallback) {
    renderCallback()
  },

  /**
   * Take a component that's already mounted into the DOM and replace its props
   * @param {ReactComponent} prevComponent component instance already in the DOM
   * @param {ReactElement} nextElement component instance to render
   * @param {DOMElement} container container to render into
   * @param {?function} callback function triggered on completion
   */
  _updateRootComponent: function (
    prevComponent,
    nextElement,
    container,
    callback
  ) {
    ReactMount.scrollMonitor(container, function () {
      ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement)
      if (callback) {
        ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback)
      }
    })

    return prevComponent
  },

  /**
   * Render a new component into the DOM. Hooked by devtools!
   *
   * @param {ReactElement} nextElement element to render
   * @param {DOMElement} container container to render into
   * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
   * @return {ReactComponent} nextComponent
   */
  _renderNewRootComponent: function (
    nextElement,
    container,
    shouldReuseMarkup,
    context
  ) {
    // Various parts of our code (such as ReactCompositeComponent's
    // _renderValidatedComponent) assume that calls to render aren't nested;
    // verify that that's the case.
    process.env.NODE_ENV !== 'production'
      ? warning(
          ReactCurrentOwner.current == null,
          '_renderNewRootComponent(): Render methods should be a pure function ' +
            'of props and state; triggering nested component updates from ' +
            'render is not allowed. If necessary, trigger nested updates in ' +
            'componentDidUpdate. Check the render method of %s.',
          (ReactCurrentOwner.current && ReactCurrentOwner.current.getName()) ||
            'ReactCompositeComponent'
        )
      : void 0

    !(
      container &&
      (container.nodeType === ELEMENT_NODE_TYPE ||
        container.nodeType === DOC_NODE_TYPE ||
        container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)
    )
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            '_registerComponent(...): Target container is not a DOM element.'
          )
        : invariant(false)
      : void 0

    ReactBrowserEventEmitter.ensureScrollValueMonitoring()
    var componentInstance = instantiateReactComponent(nextElement)

    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.

    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context
    )

    var wrapperID = componentInstance._instance.rootID
    instancesByReactRootID[wrapperID] = componentInstance

    if (process.env.NODE_ENV !== 'production') {
      ReactInstrumentation.debugTool.onMountRootComponent(componentInstance)
    }

    return componentInstance
  },

  /**
   * Renders a React component into the DOM in the supplied `container`.
   *
   * If the React component was previously rendered into `container`, this will
   * perform an update on it and only mutate the DOM as necessary to reflect the
   * latest React component.
   *
   * @param {ReactComponent} parentComponent The conceptual parent of this render tree.
   * @param {ReactElement} nextElement Component element to render.
   * @param {DOMElement} container DOM element to render into.
   * @param {?function} callback function triggered on completion
   * @return {ReactComponent} Component instance rendered in `container`.
   */
  renderSubtreeIntoContainer: function (
    parentComponent,
    nextElement,
    container,
    callback
  ) {
    !(parentComponent != null && parentComponent._reactInternalInstance != null)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(false, 'parentComponent must be a valid React Component')
        : invariant(false)
      : void 0
    return ReactMount._renderSubtreeIntoContainer(
      parentComponent,
      nextElement,
      container,
      callback
    )
  },

  _renderSubtreeIntoContainer: function (
    parentComponent,
    nextElement,
    container,
    callback
  ) {
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render')
    !ReactElement.isValidElement(nextElement)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'ReactDOM.render(): Invalid component element.%s',
            typeof nextElement === 'string'
              ? " Instead of passing a string like 'div', pass " +
                  "React.createElement('div') or <div />."
              : typeof nextElement === 'function'
                ? ' Instead of passing a class like Foo, pass ' +
                  'React.createElement(Foo) or <Foo />.'
                : // Check if it quacks like an element
                  nextElement != null && nextElement.props !== undefined
                  ? ' This may be caused by unintentionally loading two independent ' +
                    'copies of React.'
                  : ''
          )
        : invariant(false)
      : void 0

    process.env.NODE_ENV !== 'production'
      ? warning(
          !container ||
            !container.tagName ||
            container.tagName.toUpperCase() !== 'BODY',
          'render(): Rendering components directly into document.body is ' +
            'discouraged, since its children are often manipulated by third-party ' +
            'scripts and browser extensions. This may lead to subtle ' +
            'reconciliation issues. Try rendering into a container element created ' +
            'for your app.'
        )
      : void 0

    var nextWrappedElement = ReactElement(
      TopLevelWrapper,
      null,
      null,
      null,
      null,
      null,
      nextElement
    )

    var prevComponent = getTopLevelWrapperInContainer(container)

    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement
      var prevElement = prevWrappedElement.props
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance()
        var updatedCallback =
          callback &&
          function () {
            callback.call(publicInst)
          }
        ReactMount._updateRootComponent(
          prevComponent,
          nextWrappedElement,
          container,
          updatedCallback
        )
        return publicInst
      } else {
        ReactMount.unmountComponentAtNode(container)
      }
    }

    var reactRootElement = getReactRootElementInContainer(container)
    var containerHasReactMarkup =
      reactRootElement && !!internalGetID(reactRootElement)
    var containerHasNonRootReactChild = hasNonRootReactChild(container)

    if (process.env.NODE_ENV !== 'production') {
      process.env.NODE_ENV !== 'production'
        ? warning(
            !containerHasNonRootReactChild,
            'render(...): Replacing React-rendered children with a new root ' +
              'component. If you intended to update the children of this node, ' +
              'you should instead have the existing children update their state ' +
              'and render the new components instead of calling ReactDOM.render.'
          )
        : void 0

      if (!containerHasReactMarkup || reactRootElement.nextSibling) {
        var rootElementSibling = reactRootElement
        while (rootElementSibling) {
          if (internalGetID(rootElementSibling)) {
            process.env.NODE_ENV !== 'production'
              ? warning(
                  false,
                  'render(): Target node has markup rendered by React, but there ' +
                    'are unrelated nodes as well. This is most commonly caused by ' +
                    'white-space inserted around server-rendered markup.'
                )
              : void 0
            break
          }
          rootElementSibling = rootElementSibling.nextSibling
        }
      }
    }

    var shouldReuseMarkup =
      containerHasReactMarkup &&
      !prevComponent &&
      !containerHasNonRootReactChild
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      parentComponent != null
        ? parentComponent._reactInternalInstance._processChildContext(
            parentComponent._reactInternalInstance._context
          )
        : emptyObject
    )._renderedComponent.getPublicInstance()
    if (callback) {
      callback.call(component)
    }
    return component
  },

  /**
   * Renders a React component into the DOM in the supplied `container`.
   *
   * If the React component was previously rendered into `container`, this will
   * perform an update on it and only mutate the DOM as necessary to reflect the
   * latest React component.
   *
   * @param {ReactElement} nextElement Component element to render.
   * @param {DOMElement} container DOM element to render into.
   * @param {?function} callback function triggered on completion
   * @return {ReactComponent} Component instance rendered in `container`.
   */
  render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback
    )
  },

  /**
   * Unmounts and destroys the React component rendered in the `container`.
   *
   * @param {DOMElement} container DOM element containing a React component.
   * @return {boolean} True if a component was found in and unmounted from
   *                   `container`
   */
  unmountComponentAtNode: function (container) {
    // Various parts of our code (such as ReactCompositeComponent's
    // _renderValidatedComponent) assume that calls to render aren't nested;
    // verify that that's the case. (Strictly speaking, unmounting won't cause a
    // render but we still don't expect to be in a render call here.)
    process.env.NODE_ENV !== 'production'
      ? warning(
          ReactCurrentOwner.current == null,
          'unmountComponentAtNode(): Render methods should be a pure function ' +
            'of props and state; triggering nested component updates from render ' +
            'is not allowed. If necessary, trigger nested updates in ' +
            'componentDidUpdate. Check the render method of %s.',
          (ReactCurrentOwner.current && ReactCurrentOwner.current.getName()) ||
            'ReactCompositeComponent'
        )
      : void 0

    !(
      container &&
      (container.nodeType === ELEMENT_NODE_TYPE ||
        container.nodeType === DOC_NODE_TYPE ||
        container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)
    )
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'unmountComponentAtNode(...): Target container is not a DOM element.'
          )
        : invariant(false)
      : void 0

    var prevComponent = getTopLevelWrapperInContainer(container)
    if (!prevComponent) {
      // Check if the node being unmounted was rendered by React, but isn't a
      // root node.
      var containerHasNonRootReactChild = hasNonRootReactChild(container)

      // Check if the container itself is a React root node.
      var isContainerReactRoot =
        container.nodeType === 1 && container.hasAttribute(ROOT_ATTR_NAME)

      if (process.env.NODE_ENV !== 'production') {
        process.env.NODE_ENV !== 'production'
          ? warning(
              !containerHasNonRootReactChild,
              "unmountComponentAtNode(): The node you're attempting to unmount " +
                'was rendered by React and is not a top-level container. %s',
              isContainerReactRoot
                ? 'You may have accidentally passed in a React root node instead ' +
                    'of its container.'
                : 'Instead, have the parent component update its state and ' +
                    'rerender in order to remove this component.'
            )
          : void 0
      }

      return false
    }
    delete instancesByReactRootID[prevComponent._instance.rootID]
    ReactUpdates.batchedUpdates(
      unmountComponentFromNode,
      prevComponent,
      container,
      false
    )
    return true
  },

  _mountImageIntoNode: function (
    markup,
    container,
    instance,
    shouldReuseMarkup,
    transaction
  ) {
    !(
      container &&
      (container.nodeType === ELEMENT_NODE_TYPE ||
        container.nodeType === DOC_NODE_TYPE ||
        container.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE)
    )
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'mountComponentIntoNode(...): Target container is not valid.'
          )
        : invariant(false)
      : void 0

    if (shouldReuseMarkup) {
      var rootElement = getReactRootElementInContainer(container)
      if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) {
        ReactDOMComponentTree.precacheNode(instance, rootElement)
        return
      } else {
        var checksum = rootElement.getAttribute(
          ReactMarkupChecksum.CHECKSUM_ATTR_NAME
        )
        rootElement.removeAttribute(ReactMarkupChecksum.CHECKSUM_ATTR_NAME)

        var rootMarkup = rootElement.outerHTML
        rootElement.setAttribute(
          ReactMarkupChecksum.CHECKSUM_ATTR_NAME,
          checksum
        )

        var normalizedMarkup = markup
        if (process.env.NODE_ENV !== 'production') {
          // because rootMarkup is retrieved from the DOM, various normalizations
          // will have occurred which will not be present in `markup`. Here,
          // insert markup into a <div> or <iframe> depending on the container
          // type to perform the same normalizations before comparing.
          var normalizer
          if (container.nodeType === ELEMENT_NODE_TYPE) {
            normalizer = document.createElement('div')
            normalizer.innerHTML = markup
            normalizedMarkup = normalizer.innerHTML
          } else {
            normalizer = document.createElement('iframe')
            document.body.appendChild(normalizer)
            normalizer.contentDocument.write(markup)
            normalizedMarkup =
              normalizer.contentDocument.documentElement.outerHTML
            document.body.removeChild(normalizer)
          }
        }

        var diffIndex = firstDifferenceIndex(normalizedMarkup, rootMarkup)
        var difference =
          ' (client) ' +
          normalizedMarkup.substring(diffIndex - 20, diffIndex + 20) +
          '\n (server) ' +
          rootMarkup.substring(diffIndex - 20, diffIndex + 20)

        !(container.nodeType !== DOC_NODE_TYPE)
          ? process.env.NODE_ENV !== 'production'
            ? invariant(
                false,
                "You're trying to render a component to the document using " +
                  'server rendering but the checksum was invalid. This usually ' +
                  'means you rendered a different component type or props on ' +
                  'the client from the one on the server, or your render() ' +
                  'methods are impure. React cannot handle this case due to ' +
                  'cross-browser quirks by rendering at the document root. You ' +
                  'should look for environment dependent code in your components ' +
                  'and ensure the props are the same client and server side:\n%s',
                difference
              )
            : invariant(false)
          : void 0

        if (process.env.NODE_ENV !== 'production') {
          process.env.NODE_ENV !== 'production'
            ? warning(
                false,
                'React attempted to reuse markup in a container but the ' +
                  'checksum was invalid. This generally means that you are ' +
                  'using server rendering and the markup generated on the ' +
                  'server was not what the client was expecting. React injected ' +
                  'new markup to compensate which works but you have lost many ' +
                  'of the benefits of server rendering. Instead, figure out ' +
                  'why the markup being generated is different on the client ' +
                  'or server:\n%s',
                difference
              )
            : void 0
        }
      }
    }

    !(container.nodeType !== DOC_NODE_TYPE)
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            "You're trying to render a component to the document but " +
              "you didn't use server rendering. We can't do this " +
              'without using server rendering due to cross-browser quirks. ' +
              'See ReactDOMServer.renderToString() for server rendering.'
          )
        : invariant(false)
      : void 0

    if (transaction.useCreateElement) {
      while (container.lastChild) {
        container.removeChild(container.lastChild)
      }
      DOMLazyTree.insertTreeBefore(container, markup, null)
    } else {
      setInnerHTML(container, markup)
      ReactDOMComponentTree.precacheNode(instance, container.firstChild)
    }
  }
}

ReactPerf.measureMethods(ReactMount, 'ReactMount', {
  _renderNewRootComponent: '_renderNewRootComponent',
  _mountImageIntoNode: '_mountImageIntoNode'
})

module.exports = ReactMount
